package org.charles.zhifou.scoring;

import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import jakarta.annotation.Resource;
import org.charles.zhifou.common.exception.ThrowUtils;
import org.charles.zhifou.common.response.ResponseCodeEnum;
import org.charles.zhifou.constant.ZhiPuAiConstant;
import org.charles.zhifou.enums.AppTypeEnum;
import org.charles.zhifou.enums.ScoringStrategyEnum;
import org.charles.zhifou.manager.ZhiPuAiManager;
import org.charles.zhifou.model.dto.question.AiTestResultDTO;
import org.charles.zhifou.model.dto.question.QuestionAnswerDTO;
import org.charles.zhifou.model.dto.question.QuestionContentDTO;
import org.charles.zhifou.model.entity.App;
import org.charles.zhifou.model.entity.Question;
import org.charles.zhifou.model.entity.UserAnswer;
import org.charles.zhifou.service.QuestionService;
import org.redisson.api.RBucket;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;

import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * AI 测评类应用评分策略
 *
 * @author charles
 * @version 1.0
 */
@ScoringStrategyService(
        appType = AppTypeEnum.QUESTION,
        scoringStrategy = ScoringStrategyEnum.AI
)
public class AiTestScoringStrategy implements ScoringStrategy {

    @Resource
    private QuestionService questionService;

    @Resource
    private ZhiPuAiManager aiManager;

    @Resource
    private RedissonClient redissonClient;

    // 分布式锁的 key
    private static final String AI_ANSWER_LOCK = "AI_ANSWER_LOCK";

    /**
     * AI 评分结果本地缓存 (已废弃, 改为使用 Redis 缓存)
     */
    // private final Cache<String, String> answerCacheMap =
    //         Caffeine.newBuilder().initialCapacity(1024)
    //                 // 缓存 5 分钟移除
    //                 .expireAfterAccess(5L, TimeUnit.MINUTES)
    //                 .build();

    @Override
    public void doScore(UserAnswer userAnswer, App app) throws Exception {
        Long appId = app.getId();
        String choicesStr = userAnswer.getChoices();
        List<String> choices = JSONUtil.toList(choicesStr, String.class);
        String cacheKey = buildCacheKey(appId, choicesStr);

        // 注: 如果使用 RJsonBucket, 需要对 Redis 添加 ReJSON 模块, 较繁琐, 此处直接保存 String, 用 Hutool 转换
        // 详见 https://blog.csdn.net/qq_42402854/article/details/106155927

        // 如果缓存存在, 直接填入结果并返回
        RBucket<Object> aiTestResultDTOBucket = redissonClient.getBucket(cacheKey);
        if (aiTestResultDTOBucket.get() != null) {
            AiTestResultDTO aiTestResultDTO = JSONUtil.toBean((String) aiTestResultDTOBucket.get(), AiTestResultDTO.class);
            userAnswer.setResultName(aiTestResultDTO.getResultName());
            userAnswer.setResultDesc(aiTestResultDTO.getResultDesc());
            return;
        }

        // 定义锁 (应用与答案一样才加锁)
        RLock lock = redissonClient.getLock(AI_ANSWER_LOCK + cacheKey);
        try {
            // 竞争锁 (参数: 没抢到锁的线程最大等待时间, 持有锁的最长时间, 单位)
            boolean res = lock.tryLock(3L, 15L, TimeUnit.SECONDS);
            // 没抢到锁，强行返回
            ThrowUtils.throwIf(!res, ResponseCodeEnum.OPERATION_ERROR, "系统繁忙，请稍后重试");

            // 抢到锁了，执行后续业务逻辑
            // 1. 根据 id 查询到题目, 并获取到题目列表
            Question question = questionService.getOne(
                    new LambdaUpdateWrapper<Question>().eq(Question::getAppId, appId)
            );
            List<QuestionContentDTO> questionContent = JSONUtil.toList(question.getQuestionContent(), QuestionContentDTO.class);

            // 2. 调用 AI 获取结果
            // 封装 Prompt
            String userMessage = getAiTestScoringUserMessage(app, questionContent, choices);
            // AI 生成
            String result = aiManager.doSyncStableRequest(ZhiPuAiConstant.AI_TEST_SCORING_SYSTEM_MESSAGE, userMessage);
            // 截取需要的 JSON 信息
            String aiTestResultJsonStr = result.substring(result.indexOf("{"), result.lastIndexOf("}") + 1);
            AiTestResultDTO aiTestResultDTO = JSONUtil.toBean(aiTestResultJsonStr, AiTestResultDTO.class);

            // 3. 缓存结果
            // answerCacheMap.put(cacheKey, aiTestResultJsonStr);
            aiTestResultDTOBucket.set(aiTestResultJsonStr, Duration.ofHours(1L));

            // 4. 构造返回值，填充答案对象的属性
            userAnswer.setResultId(null);
            userAnswer.setResultName(aiTestResultDTO.getResultName());
            userAnswer.setResultDesc(aiTestResultDTO.getResultDesc());
            userAnswer.setResultPicture(null);
        } finally {
            if (lock != null && lock.isLocked()) {
                // 重要: 避免 A 超时导致释放了 B 的锁, C 可进入的情况
                if (lock.isHeldByCurrentThread()) {
                    lock.unlock();
                }
            }
        }

    }

    /**
     * AI 评分用户消息封装
     *
     * @param app
     * @param questionContentDTOList
     * @param choices
     * @return
     */
    private String getAiTestScoringUserMessage(App app, List<QuestionContentDTO> questionContentDTOList, List<String> choices) {
        StringBuilder userMessage = new StringBuilder();

        // 应用基本信息
        userMessage.append(app.getAppName()).append("\n");
        userMessage.append(app.getAppDesc()).append("\n");

        // 每一题的题目与答案
        List<QuestionAnswerDTO> questionAnswerDTOList = new ArrayList<>();
        for (int i = 0; i < questionContentDTOList.size(); i++) {
            QuestionAnswerDTO questionAnswerDTO = new QuestionAnswerDTO();
            questionAnswerDTO.setTitle(questionContentDTOList.get(i).getTitle());
            questionAnswerDTO.setUserAnswer(choices.get(i));
            questionAnswerDTOList.add(questionAnswerDTO);
        }
        userMessage.append(JSONUtil.toJsonStr(questionAnswerDTOList));

        return userMessage.toString();
    }

    /**
     * 缓存 key, 将答案进行压缩减小 key 的长度
     *
     * @param appId
     * @param choices
     * @return
     */
    private String buildCacheKey(Long appId, String choices) {
        return DigestUtil.md5Hex(appId + ":" + choices);
    }
}
